/**
 * @author Kenneth Arnold
 */



function alignRight(str, len, pad) {
    while (str.length < len) str = pad + str;
    return str;
}

function formatNum(val) {
    return alignRight(val.toFixed(2), 5, '0')
}

/**
 * Create a Pin object.
 * @param {Object} x
 * @param {Object} y
 * @param {Direction} orient direction of the pin
 */
function Pin(x, y, orient) {
  this.x = x;
  this.y = y;
  this.orient = orient;
}


function Meas(pin, unit) {
  SVGText.call(this, pin, pin.orient.flipped());
  
  this.pin = pin;
  this.unit = unit;
  this.setValue(0);
}
inherit(Meas, SVGText);
Meas.prototype.setValue = function(val) {
    this.setText(formatNum(val) + ' ' + this.unit);
};

/**
 * Create an MOS transistor.
 * @param {Object} x
 * @param {Object} y
 * @param {Object} r
 */
function MOS(x, y, r, type) {
  this.x=x; this.y=y; this.r=r; this.type=type.toLowerCase();
  SVGElem.call(this, 'g', {});

  // Surrounding circle
  this.circ = new SVGElem('circle', {
    cx: x,
    cy: y,
    r: r,
    fill: 'none',
    'pointer-events': 'all'
  });
  
  // Inner stuff.
  this.path = new SVGElem('path', {
    d: this.pathData(),
    x: x,
    y: y,
    'class': 'mos_path',
    'pointer-events': 'none'});

  this.appendChild(this.circ);
  this.appendChild(this.path);
  
  /* Highlight on mouse over. */
  this.circ.dom.onmouseover = function(e) {
    with(e.currentTarget) {
      setAttributeNS(null, 'fill', '#eef');
      setAttributeNS(null, 'stroke', 'black');
     }
  }
  this.circ.dom.onmouseout = function(e) {
    with(e.currentTarget) {
      setAttributeNS(null, 'fill', 'none');
      setAttributeNS(null, 'stroke', 'none');
     }
  }
}
inherit(MOS, SVGElem);
MOS.prototype.pathData = function() {
  var x = this.x, y = this.y, r = this.r;
  var gate = this.gate, p1 = this.p1, p2 = this.p2;
  
  // Construct the path data:
  var rx = p1.x; // right side x 
  var ty = y-2/3*r*Math.sin(Math.PI/3); // top y
  var lx = 2*x-rx; // left side x
  var by = 2*y-ty; // bottom y

  // main 
  var pathData = "M"+rx+' ' + p2.y +  // move to the start
    " V"+ty + // down to the start of outset
    " H"+lx + // left to the outset
    " V"+by + // line down
    " H"+rx + // back right
    " V"+p1.y;

  // gate
  var innerGateX = x - r/2;
  pathData += " M"+gate.x+' '+y;
  if (this.type == 'n') {
    pathData += " H"+innerGateX;
  } else {
    // draw pmos inversion bubble
    var bubbleRad = .1*r;
    pathData += " H"+(innerGateX-2*bubbleRad);
    pathData += " a"+bubbleRad+' '+bubbleRad+' 0 1 0 '+2*bubbleRad+' 0';
    pathData += " a"+bubbleRad+' '+bubbleRad+' 0 1 0 '+(-2*bubbleRad)+' 0';
  }
  pathData += " M"+innerGateX+' '+ty +
    " V"+by;
  return pathData;
}

MOS.prototype.__defineGetter__('gate', function() {
  return new Pin(this.x - 9/8*this.r, this.y, WEST);
});

MOS.prototype.__defineGetter__('p1', function() {
  return new Pin(this.x+2/3*this.r*Math.cos(Math.PI/3), this.y + 9/8*this.r, SOUTH);
});

MOS.prototype.__defineGetter__('p2', function() {
  return new Pin(this.p1.x, this.y - 9/8*this.r, NORTH);
});

MOS.prototype.__defineGetter__('src', function() {
  if (this.type=='n') return this.p1;
  else return this.p2;
});

MOS.prototype.__defineGetter__('drain', function() {
  if (this.type=='n') return this.p2;
  else return this.p1;
});

function Vsrc(pin) {
  this.pos = pin;
  this.len = 20;
  
  var rotation = pin.orient.flipped().compassAngle() + 10;
  var props = {
    x1: -this.len/2,
    x2: this.len/2,
    y1: 0, y2: 0,
    stroke: 'black',
    transform: 'translate('+pin.x+','+pin.y+'), rotate('+rotation+')'
  };
  SVGElem.call(this, 'line', props);
}
inherit(Vsrc, SVGElem);

function Ground(pin) {
  var pos = this.pos = pin;
  var len = this.len = 20;
  var rotation = pin.orient.flipped().compassAngle();
  this.rotation=rotation;
  
  var vspace = len/4;
  var len2 = 2/3*len;
  var len3 = 1/3*len;
  
  var pathData = 'M0 0'+
    'v' + 2*vspace +
    'm-' +len/2 + ' 0' +
    'h'+len +
    'm-' + (len/2+len2/2)+ ' ' +vspace +
    'h'+len2 +
    'm-' + (len2/2+len3/2)+ ' ' +vspace +
    'h'+len3;
  SVGElem.call(this, 'path', {
    d: pathData,
    transform: 'translate('+pos.x+','+pos.y+'), rotate('+rotation+')',
    'class': 'ground'
  });
}
inherit(Ground, SVGElem);

function Node() {
  SVGElem.call(this, 'g', {});
}
inherit(Node, SVGElem);

function Wire(start, end) {
  this.start = start; this.end = end;
  SVGElem.call(this, 'line', {
    x1: start.x, y1: start.y,
    x2: end.x, y2: end.y,
    'class': 'wire'
  });
}
inherit(Wire, SVGElem);

function Source(pin, type) {
  SVGElem.call(this, 'g', {});
  this.type = type;
  
  // Create the edit widget.
  var txt = new SVGElem('text', {
    x: pin.x,
    y: pin.y,
    'text-anchor': 'end',
    'dominant-baseline': 'central'
  });
  this.appendChild(txt);
  
  // Split out the characters.
  var spans = this.spans = new Array();
  var i;
  for (i=0; i<this.nDigits; i++) {
    spans[i] = new SVGElem('tspan', {});
    spans[i].appendChild(document.createTextNode('0'));
    txt.appendChild(spans[i]);
    if (i == this.nDigits - this.exponent - 1)
      txt.appendChild(document.createTextNode('.'));
  }
  
  // Units
  this.units = document.createTextNode(' V');
  txt.appendChild(this.units);

  this.selectDigit(1);
  this.setNum(2.5);
}
inherit(Source, SVGElem);
Source.prototype.keyHandler = function(e) {
  switch(e.keyCode) {
  case 37: // KEY_ARROW_LEFT
    this.selectDigit(this.selected-1);
    break;
  case 38: // KEY_ARROW_UP
    this.addToDigit(this.selected, 1);
    break;
  case 39: // KEY_ARROW_RIGHT
    this.selectDigit(this.selected+1);
    break;
  case 40: // KEY_ARROW_DOWN
    this.addToDigit(this.selected, -1);
    break;
  default:
      console.log(e);
  }
};
Source.prototype.wheelHandler = function(n) {
  this.addToDigit(this.selected, n);
}

Source.prototype.selectDigit = function(d) {
  if (d < 0) d = 0;
  else if (d >= this.nDigits) d=this.nDigits-1;
  forEach(this.spans, function(span) {
    span.setAttribute('fill', 'black');
  });
  this.spans[d].setAttribute('fill', 'red');
  this.selected = d;
};

Source.prototype.setNum = function(num) {
  this.setNumRaw(num * Math.pow(10, this.exponent));
};
  
Source.prototype.setNumRaw = function(numRaw) {
  var numStr = alignRight(numRaw.toFixed(0), 4, '0');
  // Detect overflow
  if (numStr.length > this.nDigits) {
    this.setNumRaw(Math.pow(10,this.nDigits)-1);
    return;
  }
  for (var i=0; i<this.nDigits; i++) {
    this.spans[i].dom.firstChild.textContent = numStr[i];
  }
  this.numRaw = parseInt(numRaw);
  this.num = this.numRaw*Math.pow(10,-this.exponent);
  step(); // FIXME: listen for this change.
};

Source.prototype.addToDigit = function(d, delta) {
  var newNum = this.numRaw + delta*Math.pow(10,this.nDigits-d-1);
  if (newNum < 0) newNum = 0; // prevent negatives, since we still can't handle it.
  this.setNumRaw(newNum);
};
  
Source.prototype.num = 0;
Source.prototype.nDigits = 4;
Source.prototype.exponent = 2;

var svg_style = ''+
    'line, path { stroke: black; stroke-width: 2; stroke-linecap: round; } '+
    '.wire {  } '+
    '.mos_path { fill: none; }';


var circ = null;

function createCircuit() {
  circ = new RootSVGElem('canvas');
  document.getElementById("circuit").appendChild(circ.dom);
  
  circ.addStylesheet(svg_style);

  circ.add('nmos', new MOS(200, 200, 40, 'N'));
  circ.add('pmos', new MOS(200, 100, 40, 'P'));
  circ.add('Vdd', new Vsrc(circ.pmos.src));
  circ.add('gnd', new Ground(circ.nmos.src));
  
  // Vdd display
  circ.add('Vdd_disp', new Meas(circ.pmos.src, 'V'));
  circ.Vdd_disp.setValue(5);
  
  var VinPin = new Pin(100, 150, WEST);
  circ.add('Vin', new Source(VinPin));
  
  var Vin_node = new Node();
  circ.appendChild(Vin_node);
  var fakepin = new Pin(circ.nmos.gate.x, 150, WEST);
  Vin_node.appendChild(new Wire(circ.nmos.gate, fakepin));
  Vin_node.appendChild(new Wire(circ.pmos.gate, fakepin));
  Vin_node.appendChild(new Wire(fakepin, VinPin));

  circ.add('Vout', new Meas(new Pin(250, 150, EAST), 'V'));

  var Vout_node = new Node();
  circ.appendChild(Vout_node);
  var fakepin2 = new Pin(circ.nmos.drain.x, (circ.nmos.drain.y+circ.pmos.drain.y)/2, EAST);
  Vout_node.appendChild(new Wire(circ.nmos.drain, fakepin2));
  Vout_node.appendChild(new Wire(circ.pmos.drain, fakepin2));
  Vout_node.appendChild(new Wire(fakepin2, circ.Vout.pin));
  
  focus(circ.Vin);

  addListener(document.documentElement, 'keydown', keyHandler);
//  canvas.focus();
}

/* Focus management. */
var focusedElem = null;
function focus(elem) {
  focusedElem = elem;
}

function keyHandler(e) {
  if (focusedElem)
    focusedElem.keyHandler(e);
}

function wheelHandler(n) {
  if (focusedElem)
    focusedElem.wheelHandler(n);
}

addWheelEvent(wheelHandler);
addLoadEvent(createCircuit);

// Keep track of our applet. (The start function in the applet calls this.)
var be = null;
function appletLoaded(applet) {
    if (!circ) {
	/* document not yet ready. */
	setTimeout(this, 10, applet);
	return;
    }
  be = applet;
  circ.be = be.getCircuit();
  step();
}

function step() {
  if (be == null || circ == null) {
    window.setTimeout(step, 50);
    return;
  }

  // Call into the applet.
  circ.Vout.setValue(circ.be.get_Vout(circ.Vin.num, 5));
}

